using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Xml;
using System.IO;
using System.Drawing;
using Microsoft.Ink;
using System.Windows.Forms;

namespace edu.stanford.hci.ideas
{
    class StrokeUtil
    {
        private static Guid STROKE_TIMESTAMP_Guid = new Guid("8A54CF58-97E6-4fc5-8F06-F8BAD2E19B22");       // MS assigned
        private static Guid STROKE_END_TIMESTAMP_Guid = new Guid("A8FD5180-63F6-11DB-BD13-0800200C9A66");   // random assignment
        private static Guid POINTS_FORCES_Guid = new Guid("D44E6BB0-63F6-11DB-BD13-0800200C9A66");          // random assignment
        private static Guid POINTS_TIMESTAMP_DELTAS_Guid = new Guid("BC653470-63F8-11DB-BD13-0800200C9A66");      // random assignment
        //private static Guid RESERVED_Guid = new Guid("1C269A20-63F9-11DB-BD13-0800200C9A66");             // random assignment

        public static Strokes GetStrokesFromISF(Stream s)
        {
            Ink loadedInk = new Ink();
            byte[] isfBytes = new byte[s.Length];

            // read in the ISF
            s.Read(isfBytes, 0, (int)s.Length);

            // load the ink into a new ink object
            // once an ink object has been "dirtied" it can never load ink again
            loadedInk.Load(isfBytes);

            return loadedInk.Strokes;
        }

        public static Strokes GetStrokesFromAnotoXML(InkCollector ic, WritingPane panel, ClusterManager clusterManager, Stream stream, int imageIndex)
        {   

            // add all the strokes to the same cluster...
            //int clusterID = clusterManager.CreateNewCluster();
            //Console.WriteLine("CS ParseAnotoXML()");
            // min and max x and y values of imported strokes sets
            int minX, minY, maxX, maxY;
            minX = minY = int.MaxValue;
            maxX = maxY = int.MinValue;

            long currentStrokeBeginTime = 0;
            long currentStrokeEndTime = 0;
            ArrayList pointForceList = null;
            ArrayList pointTimestampDeltaList = null;

            // list of strokes when parsing xml files (balee)
            ArrayList pointList = null;

            // ink object for creating Anoto strokes
            Ink ink = new Ink();

            // figure out if this is BNet/Anoto XML by looking for certain pages
            bool hasPenContainerElement = false;
            bool hasPenStrokeElement = false;

            // parse XML
            XmlTextReader reader = new XmlTextReader(stream);

            // if it's not XML, this will throw an XmlException
            while (reader.Read())
            {
                string lcname = reader.Name.ToLower();
                switch (reader.NodeType)
                {
                    case XmlNodeType.Document:
                        break;
                    case XmlNodeType.Element:
                        if (lcname == "strokes" || lcname == "page")
                        {
                            hasPenContainerElement = true;
                            if (reader.HasAttributes)
                            {
                                for (int i = 0; i < reader.AttributeCount; i++)
                                {
                                    reader.MoveToAttribute(i);
                                    lcname = reader.Name.ToLower();
                                    // get min and max x and y
                                    if (lcname == "minx")
                                    {
                                        minX = int.Parse(reader.Value);
                                    }
                                    else if (lcname == "miny")
                                    {
                                        minY = int.Parse(reader.Value);
                                    }
                                    else if (lcname == "maxx")
                                    {
                                        maxX = int.Parse(reader.Value);
                                    }
                                    else if (lcname == "maxy")
                                    {
                                        maxY = int.Parse(reader.Value);
                                    }
                                }
                            }
                            break;
                        }
                        if (lcname == "stroke")
                        {
                            hasPenStrokeElement = true;
                            pointList = new ArrayList();
                            pointForceList = new ArrayList();
                            pointTimestampDeltaList = new ArrayList();
                            for (int i = 0; i < reader.AttributeCount; i++)
                            {
                                reader.MoveToAttribute(i);
                                lcname = reader.Name.ToLower();
                                // get timestamps
                                if (lcname == "begin")
                                {
                                    currentStrokeBeginTime = long.Parse(reader.Value);
                                }
                                else if (lcname == "end")
                                {
                                    currentStrokeEndTime = long.Parse(reader.Value);
                                }
                            }
                            break;
                        }
                        if (lcname == "p")
                        {
                            int x = int.MinValue;
                            int y = int.MinValue;
                            int f = -1;
                            long t = -1;
                            if (reader.HasAttributes)
                            {
                                for (int i = 0; i < reader.AttributeCount; i++)
                                {
                                    reader.MoveToAttribute(i);
                                    // change Anoto floats to ints by multiplying by 1000 and casting
                                    if (string.Compare(reader.Name, "x", true) == 0)
                                    {
                                        float fx = float.Parse(reader.Value);
                                        x = (int)(fx * 1000);
                                        if (x > maxX) maxX = x;
                                        else if (x < minX) minX = x;
                                    }
                                    else if (string.Compare(reader.Name, "y", true) == 0)
                                    {
                                        float fy = float.Parse(reader.Value);
                                        y = (int)(fy * 1000);
                                        if (y > maxY) maxY = y;
                                        else if (y < minY) minY = y;
                                    }
                                    else if (string.Compare(reader.Name, "f", true) == 0)
                                    {
                                        f = int.Parse(reader.Value);
                                    }
                                    else if (string.Compare(reader.Name, "t", true) == 0)
                                    {
                                        t = long.Parse(reader.Value);
                                    }
                                }
                                if (x != int.MinValue && y != int.MinValue)
                                {
                                    pointList.Add(new Point(x, y));
                                    pointForceList.Add(f);
                                    pointTimestampDeltaList.Add(t);
                                }
                            }
                            break;
                        }
                        break;
                    case XmlNodeType.EndElement:
                        if (lcname == "stroke" && pointList != null)
                        {
                            Point[] strokesArray = (Point[])pointList.ToArray(typeof(Point));
                            Stroke s = ink.CreateStroke(strokesArray);
                            //Stroke s = new Stroke(strokesArray);

                            AddExtendedPropertyToStroke(s, ConvertAnotoTimeToMicrosoftTime(currentStrokeBeginTime), STROKE_TIMESTAMP_Guid);
                            AddExtendedPropertyToStroke(s, ConvertAnotoTimeToMicrosoftTime(currentStrokeEndTime), STROKE_END_TIMESTAMP_Guid);

                            String valueString = "";
                            foreach (int f in pointForceList)
                            {
                                valueString += f + ",";
                            }
                            AddExtendedPropertyToStroke(s, valueString, POINTS_FORCES_Guid);

                            valueString = "";
                            foreach (long t in pointTimestampDeltaList)
                            {
                                valueString += (t - currentStrokeBeginTime) + ",";
                            }
                            AddExtendedPropertyToStroke(s, valueString, POINTS_TIMESTAMP_DELTAS_Guid);

                            pointList = null;
                            pointForceList = null;
                            pointTimestampDeltaList = null;
                            //TabletPropertyMetrics theMetric = ink.Strokes[0].GetPacketDescriptionPropertyMetrics(PacketProperty.NormalPressure);
                            //Console.WriteLine(theMetric.Maximum);
                            //Console.WriteLine(theMetric.Minimum);
                        }
                        break;
                    case XmlNodeType.Text:
                        break;
                    // There are many other types of nodes, but
                    // we are not interested in them
                }
            }
            // if it's not BNet/Anoto XML, throw an exception
            if (!hasPenContainerElement || !hasPenStrokeElement)
            {
                throw new Exception("Non-ButterflyNet/Anoto XML file!");
            }

            Point rectPt;
            if (minX != int.MaxValue && minY != int.MaxValue && maxX != int.MinValue && maxY != int.MinValue)
            {
                rectPt = new Point((maxX - minX) / 1000, (maxY - minY) / 1000);
            }
            else
            {
                rectPt = new Point(200, 200);
            }

            // xxx the below is an approximate aspect ratio for a notebook page - should
            // calculate the actual AR from the page
            // -dakers

            double aspectRatio = 3.1 / 5.0;
            int imageHeight = panel.Width / ((int)Math.Floor(aspectRatio * 5.0));
            Point upLeft = new Point((int) Math.Floor(4.0/5.0*panel.Width), imageHeight*imageIndex);
            Point rectSize = new Point((int) Math.Floor(panel.Width/5.0), imageHeight);
            
            ic.Renderer.PixelToInkSpace(panel.Handle, ref upLeft);
            ic.Renderer.PixelToInkSpace(panel.Handle, ref rectSize);

            Strokes oldStrokes = ic.Ink.Strokes;

            Rectangle theRectangle = new Rectangle(upLeft.X, upLeft.Y, rectSize.X, rectSize.Y);
            ic.Ink.AddStrokesAtRectangle(ink.Strokes, theRectangle);

            Strokes newStrokes = ic.Ink.Strokes;

            newStrokes.Remove(oldStrokes);

            // For now, just add all the new strokes to a new cluster. 
            // Later, we can change this to cluster the strokes in space and time.

            clusterManager.CreateNewClusterFromStrokes(newStrokes);
            
            panel.Invalidate();
            return newStrokes;
        }

        public static void AddExtendedPropertyToStroke(Stroke s, String value, Guid guid)
        {
            try
            {
                //Write the time to a temporary byte array.
                MemoryStream memstr = new MemoryStream();
                BinaryWriter binwrite = new BinaryWriter(memstr);

                binwrite.Write(value);
                Byte[] bt = memstr.ToArray();

                memstr.Close();
                binwrite.Close();

                //Save the extended property on the stroke.
                s.ExtendedProperties.Add(guid, bt);
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
        }

        public static void AddExtendedPropertyToStroke(Stroke s, long value, Guid guid)
        {
            try
            {
                //Write the time to a temporary byte array.
                MemoryStream memstr = new MemoryStream();
                BinaryWriter binwrite = new BinaryWriter(memstr);

                binwrite.Write(value);
                Byte[] bt = memstr.ToArray();

                memstr.Close();
                binwrite.Close();

                //Save the extended property on the stroke.
                s.ExtendedProperties.Add(guid, bt);
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
            }
        }

        public static BinaryReader GetExtendedPropertyFromStroke(Stroke s, Guid guid)
        {
            ExtendedProperty ep = s.ExtendedProperties[guid];
            Byte[] bt = (Byte[])ep.Data;
            return new BinaryReader(new MemoryStream(bt));
        }

        public static void WriteStrokesToAnotoXML(Strokes strokes, String filename)
        {
            XmlDocument doc = new XmlDocument();
            //doc.AppendChild(doc.CreateXmlDeclaration("1.0", null, null));
            XmlElement rootElement = doc.CreateElement("strokes");
            foreach (Stroke s in strokes) {
                XmlElement strokeElement = doc.CreateElement("stroke");
                String[] pointForceList = null;
                String[] pointTimestampDeltaList = null;
                long anotoBeginTime = long.MinValue;

                if (s.ExtendedProperties.DoesPropertyExist(STROKE_TIMESTAMP_Guid))
                {
                    long msBeginTime = GetExtendedPropertyFromStroke(s, STROKE_TIMESTAMP_Guid).ReadInt64();
                    anotoBeginTime = ConvertMicrosoftTimeToAnotoTime(msBeginTime);
                    strokeElement.SetAttribute("begin", anotoBeginTime.ToString());
                }
                if (s.ExtendedProperties.DoesPropertyExist(STROKE_END_TIMESTAMP_Guid))
                {
                    long msEndTime = GetExtendedPropertyFromStroke(s, STROKE_END_TIMESTAMP_Guid).ReadInt64();
                    long anotoEndTime = ConvertMicrosoftTimeToAnotoTime(msEndTime);
                    strokeElement.SetAttribute("end", anotoEndTime.ToString());
                }
                if (s.ExtendedProperties.DoesPropertyExist(POINTS_FORCES_Guid))
                {
                    pointForceList = GetExtendedPropertyFromStroke(s, POINTS_FORCES_Guid).ReadString().Split(new char[] { ',' });
                }
                if (s.ExtendedProperties.DoesPropertyExist(POINTS_TIMESTAMP_DELTAS_Guid))
                {
                    pointTimestampDeltaList = GetExtendedPropertyFromStroke(s, POINTS_TIMESTAMP_DELTAS_Guid).ReadString().Split(new char[] { ',' });
                }

                int i = 0;
                foreach (Point p in s.GetPoints())
                {
                    XmlElement pointElement = doc.CreateElement("p");
                    pointElement.SetAttribute("x", (((float)p.X) / 1000).ToString());
                    pointElement.SetAttribute("y", (((float)p.Y) / 1000).ToString());
                    if (pointForceList != null)
                    {
                        pointElement.SetAttribute("f", pointForceList[i]);
                    }
                    else
                    {
                        pointElement.SetAttribute("f", "-1");
                    }
                    if (pointTimestampDeltaList != null)
                    {
                        pointElement.SetAttribute("t", (anotoBeginTime + int.Parse(pointTimestampDeltaList[i])).ToString());
                    }
                    else
                    {
                        pointElement.SetAttribute("t", "-1");
                    }

                    strokeElement.AppendChild(pointElement);
                    i++;
                }
                rootElement.AppendChild(strokeElement);
            }
            doc.AppendChild(rootElement);
            doc.Save(filename);
        }

        public static long ConvertAnotoTimeToMicrosoftTime(long anotoTime)
        {
            return anotoTime * 10000 + new DateTime(1970, 1, 1).ToFileTime();
        }

        public static long ConvertMicrosoftTimeToAnotoTime(long msTime)
        {
            return (msTime - new DateTime(1970, 1, 1).ToFileTime()) / 10000;
        }

        public static void PrintTimestampsForStrokes(Strokes strokes)
        {
            // code for recovering stroke timestamps
            Console.Write("Stroke timestamps: ");
            foreach (Stroke s in strokes)
            {
                if (s.ExtendedProperties.DoesPropertyExist(STROKE_TIMESTAMP_Guid))
                {
                    ExtendedProperty ep = s.ExtendedProperties[STROKE_TIMESTAMP_Guid];
                    Byte[] bt = (Byte[])ep.Data;
                    BinaryReader binread = new BinaryReader(new MemoryStream(bt));
                    long filetime = binread.ReadInt64();
                    Console.Write(filetime + ", ");
                }
            }
        }

        // This function saves the form in ISF format.
        // It uses ExtendedProperties to preserve the first and last names.
        // ExtendedProperties are an easy way to store non-ink data within an
        // ink object. In this case, there is no outer format which contains the
        // ink, so the only place to store the names is within the ink object itself.
        public static void SaveISF(InkCollector inkCollector, Stream s)
        {
            byte[] isf;

            // This is the ink object which will be serialized
            ExtendedProperties inkProperties = inkCollector.Ink.ExtendedProperties;

            // Store the name fields in the ink object
            // These fields will roundtrip through the ISF format
            // Ignore empty fields since strictly empty strings 
            //       cannot be stored in ExtendedProperties.
            //if (FirstNameBox.Text.Length > 0)
            //{
            //    inkProperties.Add(FirstName, FirstNameBox.Text);
            //}
            //if (LastNameBox.Text.Length > 0)
            //{
            //    inkProperties.Add(LastName, LastNameBox.Text);
            //}

            // Perform the serialization
            isf = inkCollector.Ink.Save(PersistenceFormat.InkSerializedFormat);

            // If the first and last names were added as extended
            // properties to the ink, remove them - these properties
            // are only used for the save and there is no need to
            // keep them around on the ink object.
            //if (inkProperties.DoesPropertyExist(FirstName))
            //{
            //    inkProperties.Remove(FirstName);
            //}
            //if (inkProperties.DoesPropertyExist(LastName))
            //{
            //    inkProperties.Remove(LastName);
            //}

            // Write the ISF to the stream
            s.Write(isf, 0, isf.Length);
        }

        // This function will load ISF into the ink object.
        // It will also pull the data stored in the object's extended properties and
        // repopulate the text boxes.
        public static void LoadISF(InkCollector inkCollector, Stream s)
        {
            Ink loadedInk = new Ink();
            byte[] isfBytes = new byte[s.Length];

            // read in the ISF
            s.Read(isfBytes, 0, (int)s.Length);

            // load the ink into a new ink object
            // once an ink object has been "dirtied" it can never load ink again
            loadedInk.Load(isfBytes);

            // temporarily disable the ink collector and swap ink objects
            inkCollector.Enabled = false;
            inkCollector.Ink = loadedInk;
            inkCollector.Enabled = true;

            ExtendedProperties inkProperties = inkCollector.Ink.ExtendedProperties;

            // Get the raw data out of this stroke's extended
            // properties list, using our previously defined 
            // Guid as a key to the extended property we want.
            // Since the save method stored the first and last
            // name information as extended propertied, we can
            // remove this information now that the load is complete.
            //if (inkProperties.DoesPropertyExist(FirstName))
            //{
            //    FirstNameBox.Text = (String)inkProperties[FirstName].Data;
            //    inkProperties.Remove(FirstName);
            //}
            //else
            //{
            //    FirstNameBox.Text = String.Empty;
            //}

            //if (inkProperties.DoesPropertyExist(LastName))
            //{
            //    LastNameBox.Text = (String)inkProperties[LastName].Data;
            //    inkProperties.Remove(LastName);
            //}
            //else
            //{
            //    LastNameBox.Text = String.Empty;
            //}
        }

        // This function will load XML into the ink object.
        // It will repopulate the text boxes using the data stored in the XML.
        public static void LoadXML(InkCollector inkCollector, Stream s)
        {
            // This object will encode our byte data to a UTF8 string
            UTF8Encoding utf8 = new UTF8Encoding();

            XmlDocument xd = new XmlDocument();
            XmlNodeList nodes;
            Ink loadedInk = new Ink();

            // Load the XML data into an XMLDocument object
            xd.Load(s);

            // Get the data in the ink node
            nodes = xd.GetElementsByTagName("Ink");

            // load the ink into a new ink object
            // once an ink object has been "dirtied" it can never load ink again
            if (0 != nodes.Count)
            {
                loadedInk.Load(utf8.GetBytes(nodes[0].InnerXml));
            }

            // temporarily disable the ink collector and swap ink objects
            inkCollector.Enabled = false;
            inkCollector.Ink = loadedInk;
            inkCollector.Enabled = true;
        }

        // This function saves the form in XML format.
        // It uses the base64 encoded version of the ink, which is most suitable
        // for representation in XML. The names are stored as XML fields, rather
        // than custom properties, so that these properties can be most easily
        // accessible if the XML is saved in a database.
        public static void SaveXML(InkCollector inkCollector, Stream s)
        {
            // This object will encode our byte data to a UTF8 string
            UTF8Encoding utf8 = new UTF8Encoding();

            byte[] base64ISF_bytes;
            string base64ISF_string;

            // Create a new XmlTextWriter.
            XmlTextWriter xwriter = new XmlTextWriter(s, System.Text.Encoding.UTF8);

            // Write the beginning of the document including the 
            // document declaration. 
            xwriter.WriteStartDocument();

            // Write the beginning of the "data" element. This is 
            // the opening tag to our data 
            xwriter.WriteStartElement("SerializationSampleData");

            // Get the base64 encoded ISF
            base64ISF_bytes = inkCollector.Ink.Save(PersistenceFormat.Base64InkSerializedFormat);

            // Convert it to a String
            base64ISF_string = utf8.GetString(base64ISF_bytes);

            // Write the ISF containing node to the XML
            xwriter.WriteElementString("Ink", base64ISF_string);

            // Write the text date from the form
            // Note that the names are stored as XML fields, rather
            // than custom properties, so that these properties can 
            // be most easily accessible if the XML is saved in a database.
            //xwriter.WriteElementString("FirstName", FirstNameBox.Text);
            //xwriter.WriteElementString("LastName", LastNameBox.Text);

            //End the "data" element.
            xwriter.WriteEndElement();

            //End the document
            xwriter.WriteEndDocument();

            //Close the xml document.
            xwriter.Close();
        }

        // This function saves the form in HTML format.
        // It also creates an ink fortified GIF which is referenced by the HTML.
        // A fortified GIF is an image file that has ISF embedded inside it. To an
        // ink-unaware application, it is simply an image, but to an ink-aware
        // application, all of the original ink data is available.
        //
        // Please refer to the "Ink Interoperability" whitepaper for more information
        // on fortified GIFs.
        public static void SaveHTML(InkCollector inkCollector, Stream s, String nameBase)
        {

            // It is not possible to save an empty .gif, so it 
            // is first necessary to ensure that the ink has a bounding
            // box (i.e. the ink is not empty).  The bounding box is 
            // used for this check instead of checking whether the stroke
            // count is zero, since the ink could still be empty if the
            // strokes don't contain any points.
            if (inkCollector.Ink.Strokes.GetBoundingBox().IsEmpty)
            {
                MessageBox.Show("Unable to save empty ink in HTML persistence format.");
            }
            else
            {

                FileStream gifFile;
                byte[] fortifiedGif = null;
                String html;
                byte[] htmlBytes;

                // This object will encode our byte data to a UTF8 string
                UTF8Encoding utf8 = new UTF8Encoding();

                // Create a directory to store the fortified GIF which also contains ISF
                // and open the file for writing
                Directory.CreateDirectory(nameBase + "_files");
                gifFile = File.OpenWrite(nameBase + "_files\\signature.gif");

                // Generate the fortified GIF represenation of the ink
                fortifiedGif = inkCollector.Ink.Save(PersistenceFormat.Gif);

                // Write and close the gif file
                gifFile.Write(fortifiedGif, 0, fortifiedGif.Length);
                gifFile.Close();

                // Create the HTML output
                // Note that the names are stored in HTML tags, rather
                // than custom properties, so that these properties can 
                // be easily retrieved from the HTML.
                html =
                    "<HTML><HEAD></HEAD><BODY>" +

                    //"<P>First Name: " +
                    //FirstNameBox.Text +
                    //" &nbsp;&nbsp;&nbsp;Last Name: " +
                    //LastNameBox.Text +
                    //"</P>" +

                    "<P><IMG src='" +
                    nameBase + "_files\\signature.gif" +
                    "' /></P>" +

                    "</BODY></HTML>";

                // Convert the HTML to a byte array for writing to the stream
                htmlBytes = utf8.GetBytes(html);

                // Write the HTML to the stream
                s.Write(htmlBytes, 0, htmlBytes.Length);
            }
        }

    }
}
